package flight.services.amf
{
	import flash.net.ObjectEncoding;
	import flash.utils.ByteArray;
	
	public class AMFMessage
	{
		public static const AMF_UNKNOWN_CONTENT_LENGTH:int	= -1;
		
		public var version:uint = ObjectEncoding.AMF3;
		public var headers:Array = new Array();
		public var bodies:Array = new Array();
		
		protected var amf3Parser:AMF3Parser = new AMF3Parser();
		protected var amf0Parser:AMF0Parser = new AMF0Parser(amf3Parser);
		
		protected function clearAllReferences():void
		{
			amf0Parser.clearAllReferences();
			amf3Parser.clearAllReferences();
		}		
		
		public function get dataObjects():Array
		{
			var values:Array = new Array();
			if (bodies !== null) {
				for each (var body:Object in bodies) {
					values.push((body as AMFBody).data);
				}					
			}
			return values;
		}
		
		public function get firstDataObject():Object
		{
			var value:Object = null;
			if (bodies !== null && bodies.length > 0) {
				value = bodies[0];
				if (value.hasOwnProperty('data')) {
					value = value.data;
				}					
			}
			return value;
		}
		
		public function addHeader(name:String=null, mustUnderstand:Boolean=false, data:Object=null):void
		{
			var amfHeader:AMFHeader = new AMFHeader();
				amfHeader.headerName = name;
				amfHeader.mustUnderstand = mustUnderstand;
				amfHeader.data = data;
				
			headers.push(amfHeader);
		}
		
		public function addBody(targetURI:String=null, responseURI:String=null, data:Object=null):void
		{
			var amfBody:AMFBody = new AMFBody();
				amfBody.targetURI = targetURI;
				amfBody.responseURI = responseURI;
				amfBody.data = data;
				
			bodies.push(amfBody);
		}
		
		public function readMessage(src:ByteArray):Boolean
		{
 			version = ObjectEncoding.AMF0;
			headers = new Array();
			bodies = new Array();
			clearAllReferences();
			
			// Since this is an AMF message packet, 
			// start processing objects using the AMF0 encoding 
			// until we encounter an AMF0_AMF3 promotion type marker
			// as per the AMF3 and AMF0 file format documents, 
			// AMF serialized objects are of type AMF0 until the special AMF0_AMF3 marker is encountered.
			// once this marker has been found, we can switch to AMF3 encoding.
			var orgObjectEncoding:uint = src.objectEncoding;
			src.objectEncoding = ObjectEncoding.AMF0;
			
			// parse version
			version = src.readUnsignedShort();
			if (version != ObjectEncoding.AMF0 && version != ObjectEncoding.AMF3) {
				// unknown AMF version
				// throw new Error("Unknown AMF Version: " + version);
				
				src.position -= 2;
				
				// see if it's an amf object that we can parse
				var attempResult:Object = new Object();
				if (attemptReadObject(src, attempResult)) {
					var amfBodyTest:AMFBody = new AMFBody();
					amfBodyTest.data = attempResult.data;
					bodies.push(amfBodyTest);
					return true;
				}
				
				src.position += 2;
			}
			
			// parse headers
			var amfHeader:AMFHeader;
			var numHeaders:uint = src.readUnsignedShort();
			while (numHeaders--) {
				amfHeader = readHeader(src);
				if (amfHeader == null) { 
					return false; 
				}
				headers.push(amfHeader);
			}
			
			// parse bodies
			var amfBody:AMFBody;
			var numBodies:uint = src.readUnsignedShort();
			while (numBodies--) {
					amfBody = readBody(src);
				if (amfBody == null) { 
					return false; 
				}
				bodies.push(amfBody);
			}			
			
			// restore the original object encoding, just to be nice.  
			// callers won't expect a function to be changing the encoding of a ByteArray.
			src.objectEncoding = orgObjectEncoding;
			return true;
		}		
		
		public function writeMessage(dst:ByteArray):void
		{
			clearAllReferences();
			
			// all messages start with AMF0, subsequent bodies may switch to AMF3 later
			dst.objectEncoding = ObjectEncoding.AMF0;
			
			// write message object encoding version
			dst.writeShort(ObjectEncoding.AMF3);
			
			// write headers
			dst.writeShort(headers.length);
			for each (var header:AMFHeader in headers) {
				writeHeader(dst, header);
			}
			
			// write bodies
			dst.writeShort(bodies.length);
			for each (var body:AMFBody in bodies) {
				writeBody(dst, body);
			}			
		}
		
		protected function readHeader(src:ByteArray):AMFHeader
		{
			var amfHeader:AMFHeader = new AMFHeader();
			
			try {
				amfHeader.headerName = src.readUTF();
				amfHeader.mustUnderstand = (src.readByte() != 0); 
				var length:uint = src.readUnsignedInt(); // ignore header length
				amfHeader.data = readTypeMarkerDataObject(src);
			} catch (err:Error) { 
				amfHeader = null; 
			}
			
			return amfHeader;
		}
		
		protected function writeHeader(dst:ByteArray, amfHeader:AMFHeader):void
		{
			dst.writeUTF(amfHeader.headerName);
			dst.writeByte(amfHeader.mustUnderstand? 1: 0);
			dst.writeUnsignedInt(AMF_UNKNOWN_CONTENT_LENGTH);
			writeTypeMarkerDataObject(dst, amfHeader.data);
		}
		
		protected function readBody(src:ByteArray):AMFBody
		{
			var amfBody:AMFBody = new AMFBody();
			
			try {
				amfBody.targetURI = src.readUTF();
				amfBody.responseURI = src.readUTF();
				var length:uint = src.readUnsignedInt(); // ignore body length	
				amfBody.data = readTypeMarkerDataObject(src);
			} catch (err:Error) {
				amfBody = null;
			}
			
			return amfBody;
		}
		
		protected function writeBody(dst:ByteArray, amfBody:AMFBody):void
		{
			if (amfBody.targetURI == null) { amfBody.targetURI = ''; }
			if (amfBody.responseURI == null) { amfBody.responseURI = ''; }

			dst.writeUTF(amfBody.targetURI);
			dst.writeUTF(amfBody.responseURI);
			dst.writeUnsignedInt(AMF_UNKNOWN_CONTENT_LENGTH);
			
			// if the encoding isn't AMF3 yet, switch to it.
			if (dst.objectEncoding !== ObjectEncoding.AMF3) {
				dst.writeByte(AMF0Parser.AMF0_AMF3);
				dst.objectEncoding = ObjectEncoding.AMF3;			
			}
			
			writeTypeMarkerDataObject(dst, amfBody.data);
		}
		
		protected function readTypeMarkerDataObject(src:ByteArray):Object
		{
			if (src.objectEncoding == ObjectEncoding.AMF0) {
				return amf0Parser.readTypeMarkerDataObject(src);
			} else if (src.objectEncoding == ObjectEncoding.AMF3) {
				return amf3Parser.readTypeMarkerDataObject(src);
			}
			return null;
		}

		protected function writeTypeMarkerDataObject(dst:ByteArray, data:Object):void
		{
			if (dst.objectEncoding == ObjectEncoding.AMF0) {
				amf0Parser.writeTypeMarkerDataObject(dst, data);
			} else if (dst.objectEncoding == ObjectEncoding.AMF3) {
				amf3Parser.writeTypeMarkerDataObject(dst, data);
			}
		}

		protected function attemptReadObject(src:ByteArray, result:Object):Boolean
		{
			// if successful, put the response data on result.data and return true
			var success:Boolean = false;
			var posOrg:uint = src.position;
			var objectEncodingOrg:uint = src.objectEncoding;
				
			try {
				src.objectEncoding = ObjectEncoding.AMF3;
				result.data = src.readObject();
				success = (src.bytesAvailable == 0);				
			} catch (err:Error) {
				success = false;
			}
			
			if (!success) {
				src.position = posOrg;
				src.objectEncoding = objectEncodingOrg;
				result.data = null;
			}
			
			return success;
		}
	}
}
	
	
class AMFHeader
{
	public var headerName:String;
	public var mustUnderstand:Boolean = false;
	public var data:Object;
}

class AMFBody
{
	public var targetURI:String;
	public var responseURI:String;
	public var data:Object;
}
